React Hooks
Hook は、React Component と同様に 純粋関数 であるべき React は、Hook が毎回同じ場所で呼び出されることに依存している したがって、常に同じ順序で呼ばれる必要がある
理由
再レンダリング時に再び useState が呼び出されたときに、どの状態を返すべきかを特定しているため React はすべてのコンポーネントに対して state のペアの配列を保持しています。
また、現在のペアインデックスも管理しており、レンダー前に 0 に設定されます。
useState が呼び出されるたびに、React は次の状態ペアを提供し、インデックスをインクリメントします。
簡易的な useState の実装
code:js
// React内部でのuseStateの動作
function useState(initialState) {
if (pair) {
// 最初のレンダリングではないので、すでにstateのペアが存在している。
// それを返し、次のフック呼び出しに備える。
currentHookIndex++;
return pair;
}
// これは初めてのレンダリングなので、stateのペアを作成し、それを保存する。
function setState(nextState) {
// ユーザーがstateの変更を要求したとき、新しい値をペアに設定する。
updateDOM();
}
// 今後のレンダリングのためにペアを保存し、次のフック呼び出しに備える。
currentHookIndex++;
return pair;
}
Hook はフックはトップレベルでのみ呼び出す というルールを守ると、Hook は常に同じ順序で呼ばれる
逆に条件分岐内で Hook を呼び出すと、同じ順序で呼ばれなくなる
ESLint の eslint-plugin-react-hooks プラグインを入れると、違反しないように手助けしてくれる code:sh
$ npm install --save-dev eslint-plugin-react-hooks
code:eslint.config.js
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
import pluginReact from "eslint-plugin-react";
import eslintConfigPrettier from "eslint-config-prettier";
import eslintPluginReactHooks from "eslint-plugin-react-hooks";
export default [
{ languageOptions: { globals: globals.browser } },
pluginJs.configs.recommended,
...tseslint.configs.recommended,
{
...pluginReact.configs.flat.recommended,
settings: { react: { version: "detect" } },
plugins: {
"react-hooks": eslintPluginReactHooks,
},
rules: {
"react/react-in-jsx-scope": "off",
...eslintPluginReactHooks.configs.recommended.rules,
},
},
eslintConfigPrettier,
];
2024 年 9 月時点で、Flat Config 向けの共有設定(Shareable Configs)が提供されていないため、少々設定方法が異なる Hook の呼び出す順序が一貫している限り、直接 Component から呼び出されるか、他の関数を通じて間接的に呼び出されるかは問題でない
これより、Hook は React の関数からのみ呼び出す というルールが導ける
カスタムフック
他の Hook を呼び出す関数も Hook として扱って 呼び出される順序を保持する 必要がある
このような Hook がカスタムフック
慣例的に use という接頭辞を関数名に付ける
e.g.
code:useSlideIndex.tsx
import { useState } from "react";
export const useSlideIndex = () => {
};
as const
readonly(変更不可)なリテラル型であることを示す
const useSlideIndex: () => readonly [number, React.Dispatch<React.SetStateAction<number>>]
付けないと、 (number | React.Dispatch<React.SetStateAction<number>)[] と解釈される
TypeScript はより汎用的な型に推論するため